package dev.ece.suit.loader.models.dae;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import dev.ece.core.cells.model.action.ActionManager.Action;
import dev.ece.suit.loader.Face;
import dev.ece.suit.loader.Joint;
import dev.ece.suit.loader.Material;
import dev.ece.suit.loader.Mesh;
import dev.ece.suit.loader.ModelDepthShader;
import dev.ece.suit.loader.ModelLoader;
import dev.ece.suit.loader.ModelShader;
import dev.ece.suit.loader.models.dae.DAEAnimations.Animation;
import dev.ece.suit.loader.models.dae.DAEControllers.Controller;
import dev.ece.suit.loader.models.dae.DAEGeometries.Geometry;
import dev.ece.suit.loader.models.dae.DAEGeometries.Input;
import dev.ece.suit.loader.models.dae.DAEGeometries.Source;
import dev.ece.suit.loader.models.dae.DAEVisualScenes.VisualScene;
import dev.ece.util.lib.Matrix4f;
import dev.ece.util.lib.Vector3f;
import dev.ece.util.tools.Constants;
import dev.ece.util.tools.JAXBUtils;

public class DAEModelLoader implements ModelLoader<Float> {
	
	protected static final Logger LOG = LogManager.getLogger(DAEModelLoader.class);
	
	private DAEData data;
	
	private String id;
	
	private float totaltime = 0;
	
	private int frameCount = 0;
	
	private Map<String, String> images;
	
	private Map<String, Controller> controllers;
	
	private Map<String, Integer> materialIndexMap = new ConcurrentHashMap<String, Integer>();
	
	private Controller controller;
	
	private DAEVisualScenes.Node jointNode;
	
	private DAEVisualScenes.Node modelNode;
	
	private Material[] materials;
	
	private Geometry[] geometries;
	
	private DAEMesh[] meshes;
	
	private DAEJoint[] updateJoints;
	
	private DAEJoint[] joints;
	
	private Vector3f[] boundBox;
	
	public DAEModelLoader(InputStream input) {
		StringBuffer xml = Constants.loadStringSource(input);
		data = JAXBUtils.fromXML(xml.toString(), DAEData.class);
	}
	
	@Override
	public void load() throws IOException {
		//只加载一个场景
		id = data.getScene().getInstance_visual_scene().getUrl();
		images = data.getLibrary_images().createImageMap();
		//只加载一个场景
		VisualScene visualScene = data.getLibrary_visual_scenes().getVisualScene(id);
		if(visualScene == null) {
			throw new NullPointerException("没有找到visual_scene");
		}
		LOG.debug("加载场景：{}", id);
		modelNode = visualScene.getModelNode();
		jointNode = visualScene.getJointNode();
		
		String controllerId = modelNode != null ? modelNode.getInstance_controller().getUrl() : null;
		
		materials = data.getLibrary_effects().toMeterials(images, materialIndexMap);
		data.getLibrary_materials().fillMaterialIndexMap(materialIndexMap);
		LOG.debug("材质信息数量：{}", materials.length);
		
		controllers = data.getLibrary_controllers().toControllerMap();
		
		Geometry geometry = null;
		if(StringUtils.isNotBlank(controllerId)) {
			controller = controllers.get(controllerId);
			geometry = data.getLibrary_geometries().getGeometry(controller.getSkin().getId());
		}
		if(geometry != null) {
			geometries = new Geometry[] {geometry};
		} else {
			geometries = data.getLibrary_geometries().getGeometry();
		}
		this.updateJoints = createUpdateJoints();
		LOG.debug("更新使用关节数量：{}", updateJoints.length);
		//设置动画数据
		this.setKeyFrameData();
		this.joints = createJoints();
		LOG.debug("渲染使用关节数量：{}", joints.length);
		
		this.meshes = createMeshAndTriangles();
		LOG.debug("网格数量：{}", meshes.length);
		LOG.debug("总时长：{}", this.totaltime);
		LOG.debug("总帧数：{}", this.frameCount);
		checkData();
	}
	
	/**
	 * 校验数据，如果想拓展关节数量，请重写此方法
	 */
	protected void checkData() {
		try {
			//由于shader只支持64个关节矩阵
			if(joints.length > ModelShader.MAX_SKELETON_COUNT) {
				throw new IllegalMonitorStateException(String.format("渲染使用关节数量不能超过%s个", ModelShader.MAX_SKELETON_COUNT));
			}
		} catch(IllegalMonitorStateException e) {
			this.dispose();
			throw e;
		}
	}
	
	protected void setKeyFrameData() throws IOException {
		if(updateJoints != null && data.getLibrary_animations() != null) {
			Map<String, Animation> map = data.getLibrary_animations().toAnimationMap();
			Animation animation = null;
			for(DAEJoint joint: updateJoints) {
				animation = map.get(joint.getName());
				if(animation != null) {
					joint.setKeyFrames(animation.toDAEKeyFrames());
					if(joint.getKeyFrames() != null) {
						this.totaltime = Math.max(this.totaltime, joint.getKeyFrames()[joint.getKeyFrames().length - 1].getTime());
						this.frameCount = Math.max(this.frameCount, joint.getKeyFrames().length);
					}
				}
			}
		}
	}
	
	protected DAEJoint[] createJoints() throws IOException {
		if(controller != null && this.updateJoints != null) {
			Map<String, Source> skinSources = controller.getSkin().createSourceMap();
			Input[] inputs = controller.getSkin().getJoints().getInput();
			Source[] orderedSource = new Source[2];
			
			for(Input input: inputs) {
				if(StringUtils.equals(DAE.JOINT, input.getSemantic())) {
					orderedSource[0] = skinSources.get(input.getSource());
				} else if(StringUtils.equals(DAE.INV_BIND_MATRIX, input.getSemantic())) {
					orderedSource[1] = skinSources.get(input.getSource());
				}
			}
			List<String> nameArray = new ArrayList<String>();
			List<DAEJoint> list = new ArrayList<DAEJoint>();
			String[] names = orderedSource[0].toStringArray();
			Matrix4f[] invMatrix = orderedSource[1].toMatrix4fArray();
			for(String name: names) {
				nameArray.add(name);
			}
			int index = -1;
			for(DAEJoint joint: this.updateJoints) {
				index = nameArray.indexOf(joint.getName());
				if(index > -1) {
					joint.setOffsetMatrix(invMatrix[index]);
					list.add(joint);
				}
				joint.setup();
			}
			return list.toArray(new DAEJoint[] {});
		}
		return updateJoints;
	}
	
	protected DAEJoint[] createUpdateJoints() {
		List<DAEJoint> list = new ArrayList<DAEJoint>();
		if(jointNode != null) {
			addDAEJoint(new DAEVisualScenes.Node[] {jointNode}, list, null);
		}
		return list.isEmpty() ? null : list.toArray(new DAEJoint[] {});
	}
	
	protected void addDAEJoint(DAEVisualScenes.Node[] nodes, List<DAEJoint> list, DAEJoint parent) {
		if(nodes != null) {
			DAEJoint joint = null;
			for(DAEVisualScenes.Node node: nodes) {
				joint = node.toDAEJoint(parent);
				list.add(joint);
				addDAEJoint(node.getNode(), list, joint);
			}
		}
	}
	
	protected DAEMesh[] createMeshAndTriangles() throws IOException {
		List<DAEMesh> mesh = new ArrayList<DAEMesh>();
		int len = geometries.length;
		DAETriangles triangles = null;
		this.boundBox = new Vector3f[] {new Vector3f(), new Vector3f()};
		for(int i=0; i<len; i++) {
			triangles = DAETriangles.createDAETriangles(geometries[i].getMesh(), this.joints, this.controller);
			mesh.addAll(triangles.toDAEMesh(geometries[i].getMesh(), materialIndexMap));
			triangles.boundBox(boundBox[0], boundBox[1], i == 0);
		}
		return mesh.toArray(new DAEMesh[] {});
	}

	@Override
	public Vector3f[] boundBox() {
		return this.boundBox;
	}

	@Override
	public Float total() {
		return totaltime;
	}

	@Override
	public int frameCount() {
		return this.frameCount;
	}

	@Override
	public Material[] getMaterials() {
		return this.materials;
	}

	@Override
	public Mesh[] getMeshes() {
		return this.meshes;
	}

	@Override
	public Face[] getFaces() {
		return null;
	}

	@Override
	public Joint<Float>[] getJoints() {
		return joints;
	}

	@Override
	public Joint<Float>[] getUpdateJoints() {
		return updateJoints;
	}

	@Override
	public ModelShader getModelShader() {
		return ModelShader.getDefaultModelShader();
	}

	@Override
	public ModelDepthShader getModelDepthShader() {
		return ModelDepthShader.getDefaultModelDepthShader();
	}

	@Override
	public void addAction(Map<String, Action<Float>> actions) {
		actions.put("all", new Action<Float>("all", 0f, this.totaltime, true));
	}

	@Override
	public void dispose() {
		if(images != null) {
			images.clear();
			images = null;
		}
		if(images != null) {
			controllers.clear();
			controllers = null;
		}
		if(images != null) {
			materialIndexMap.clear();
			materialIndexMap = null;
		}
		materials = null;
		geometries = null;
		meshes = null;
		jointNode = null;
		modelNode = null;
		data = null;
	}

}
